/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package extasysftpserver;

import Extasys.DataFrame;
import Extasys.Network.TCP.Server.Listener.Exceptions.ClientIsDisconnectedException;
import Extasys.Network.TCP.Server.Listener.Exceptions.OutgoingPacketFailedException;
import Extasys.Network.TCP.Server.Listener.TCPClientConnection;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.util.Hashtable;

/**
 *
 * @author nsiatras
 */
public class FTPServer extends Extasys.Network.TCP.Server.ExtasysTCPServer
{

    private final Charset UTF8_CHARSET = Charset.forName("UTF-8");
    private Hashtable fUsers = null;
    private String fRootPath;
    private InetAddress fListenerInetAddress;
    private final Object fLock = new Object();

    public FTPServer(String rootPath) throws UnknownHostException
    {
        super("", "Welcome to my FTP Server", 8, 8);

        fRootPath = rootPath;

        this.fUsers = new Hashtable<String, String>();

        fListenerInetAddress = InetAddress.getByName("127.0.0.1");

        this.AddListener("", fListenerInetAddress, 21, 100, 1024, 0, 10, "\r\n");

        fUsers.put("nsi", "1234");
    }

    @Override
    public void OnClientConnect(TCPClientConnection client)
    {
        System.out.println("New client connected to FTP Server");
        ReplyToClient(client, "220 Welcome to my Server");
    }

    @Override
    public void OnClientDisconnect(TCPClientConnection client)
    {
        System.out.println("Client disconnected from FTP Server");
        if (client.getTag() != null)
        {
            ClientProperties props = (ClientProperties) client.getTag();

            if (props.getPassiveServer() != null)
            {
                try
                {
                    // Try to dispose the passive server
                    // IF the server is disposed then an exception will come up
                    props.getPassiveServer().Dispose();
                }
                catch (Exception ex)
                {
                    System.err.println("Failed to dispose Passive server from client!" + ex.getMessage());
                }
                props.setPassiveServer(null);
                client.setTag(null);
            }

        }
    }

    @Override
    public void OnDataReceive(TCPClientConnection sender, DataFrame data)
    {

        String response = "";
        try
        {
            response = new String(data.getBytes(), UTF8_CHARSET);
            String command = response.split(" ")[0].toUpperCase();

            switch (command)
            {
                case "USER":
                    String username = response.split(" ")[1];
                    if (fUsers.containsKey(username))
                    {
                        sender.setName(username);
                        ClientProperties prop = new ClientProperties();
                        sender.setTag(prop); // Client is not yet connected

                        // ASK FOR PASSWORD
                        ReplyToClient(sender, "331 Restricted, Password?");
                    }
                    else if (response.split(" ")[1].toUpperCase().equals("ANONYMOUS"))
                    {
                        //ReplyToClient(sender, "500 Account?");
                        ReplyToClient(sender, "500 User not found");
                    }
                    else
                    {
                        ReplyToClient(sender, "500 User not found");
                    }
                    return; // RETURN!!!!

                case "PASS":
                    if (fUsers.containsKey(sender.getName()) && sender.getTag() != null)
                    {
                        String password = response.split(" ")[1].trim();
                        if (!fUsers.get(sender.getName()).equals(password))
                        {
                            ReplyToClient(sender, "500 Wrong password");
                        }
                        else
                        {
                            // User is logged successfully
                            ClientProperties prop = (ClientProperties) sender.getTag();
                            prop.setIsAuthenticated(true);
                            prop.fCurrentDirectoryPath = fRootPath;
                            sender.setTag(prop); // Client is connected!!!

                            ReplyToClient(sender, "230 User logged in to FTP Server.");
                        }
                    }
                    else
                    {
                        ReplyToClient(sender, "500 Wrong password");
                    }
                    return; // RETURN!!!!
                }

            if (sender.getTag() == null)
            {
                ReplyToClient(sender, "500 Wrong password");
                return;
            }

            ClientProperties clientProperties = (ClientProperties) sender.getTag();

            if (!clientProperties.isAuthenticated())
            {
                ReplyToClient(sender, "500 User is not connected!");
                return;
            }

            switch (command)
            {
                case "SYST":
                    ReplyToClient(sender, "215 UNIX Type: L8");
                    //ReplyToClient(sender, "215 WINDOWS_NT");
                    break;

                case "FEAT":
                    FEAT(sender);
                    break;

                case "TYPE":
                    ReplyToClient(sender, "230 OK");
                    break;

                case "PWD":
                    ReplyToClient(sender, "257 " + GetVirtualFTPPathFromSystemPath(clientProperties.fCurrentDirectoryPath));
                    break;

                case "CDUP":
                    CDUP(sender);
                    break;

                case "CWD":
                    CWD(sender, response);
                    break;

                case "PASV": // Passive Server
                    String t1[] = fListenerInetAddress.getHostAddress().replace(".", "-").split("-");
                    String IP1 = t1[0];
                    String IP2 = t1[1];
                    String IP3 = t1[2];
                    String IP4 = t1[3];

                    if (clientProperties.getPassiveServer() != null && clientProperties.getPassiveServer().fIsRunning == false)
                    {
                        //clientProperties.getPassiveServer().Stop();
                    }

                    // Try to create a new passive server
                    boolean started = false;
                    int counter = 0;
                    int DTPPort = GetRandomFreeTCPPort();

                    while (!started && counter < 15)
                    {
                        try
                        {
                            PassiveServerDTP passiveDTP = new PassiveServerDTP(this, fListenerInetAddress, DTPPort);
                            clientProperties.setPassiveServer(passiveDTP);
                            clientProperties.getPassiveServer().Start();
                            sender.setTag(clientProperties);
                            started = true;
                        }
                        catch (Exception ex)
                        {
                            counter++;
                            DTPPort = (int) (Math.random() * 1000) + 10000; // Find a new port
                        }
                    }

                    ReplyToClient(sender, "227 Entering Passive Mode (" + IP1 + "," + IP2 + "," + IP3 + "," + IP4 + "," + DTPPort / 256 + "," + DTPPort % 256 + ").");
                    break;

                case "PORT": // Active Server
                    ReplyToClient(sender, "550 NO ACTIVE SERVER YET");
                    break;

                case "LIST": // List directory
                    if (clientProperties.isConnectionPassive() && clientProperties.getPassiveServer() != null)
                    {
                        clientProperties.getPassiveServer().PrepareToSendFilesList(sender, clientProperties);
                    }
                    break;

                case "RETR": // Download file
                    File file = new File(clientProperties.fCurrentDirectoryPath + File.separator + response.substring(5));
                    if (file.exists())
                    {
                        clientProperties.getPassiveServer().PrepareToSendFile(sender, file);
                    }
                    else
                    {
                        System.err.println("File does not exist");
                        ReplyToClient(sender, "550 File " + response.substring(5) + " does not exist");
                    }
                    break;

                case "STOR": // Upload file
                    clientProperties.getPassiveServer().PrepareToReceiveFile(sender, response.substring(4));
                    break;

                case "DELE": // Delete file
                    DELE(sender, response.substring(4));
                    break;

                case "RMD": // Delete folder
                    RMD(sender, response.substring(4));
                    break;

                case "MKD": // Create directory
                    MKD(sender, response.substring(3));
                    break;

                default:
                    ReplyToClient(sender, "550 Unknown Command '" + response + "'");
                    break;
            }
        }
        catch (Exception ex)
        {
            System.err.println("FTPServer OnDataReceived: " + ex.getMessage());
            System.err.println("---- Response:" + response);
        }

        //System.out.println(response);
    }

    private void RMD(TCPClientConnection sender, String file)
    {
        ClientProperties clientProperties = (ClientProperties) sender.getTag();
        String filePath = clientProperties.fCurrentDirectoryPath + File.separator + file.trim();
        File f = new File(filePath);

        if (f.exists() && f.isDirectory())
        {
            if (f.listFiles().length == 0)
            {
                if (f.delete())
                {
                    ReplyToClientSynchronously(sender, "230 OK");
                }
            }
            else
            {
                ReplyToClientSynchronously(sender, "550 Directory " + file + " is not empty!");
            }
        }
        else
        {
            ReplyToClientSynchronously(sender, "550 Directory " + file + " not found!");
        }
    }

    private void DELE(TCPClientConnection sender, String file)
    {
        ClientProperties clientProperties = (ClientProperties) sender.getTag();
        String filePath = clientProperties.fCurrentDirectoryPath + File.separator + file.trim();
        File f = new File(filePath);
        if (f.exists() && f.isFile())
        {
            if (f.delete())
            {
                ReplyToClientSynchronously(sender, "230 OK");
            }
            else
            {
                ReplyToClientSynchronously(sender, "550 Failed to delete file " + file);
            }
        }
        else
        {
            ReplyToClientSynchronously(sender, "550 File not found!");
        }
    }

    private void MKD(TCPClientConnection sender, String directoryName)
    {
        ClientProperties clientProperties = (ClientProperties) sender.getTag();

        String systemPath = clientProperties.fCurrentDirectoryPath + File.separator + directoryName.trim();
        File f = new File(systemPath);

        if (!f.exists())
        {
            try
            {
                if (f.mkdir())
                {
                    ReplyToClient(sender, "230 OK");
                }

            }
            catch (Exception ex)
            {
                ReplyToClient(sender, "550 Cannor create directory. Please check server permissions");
            }
        }
        else
        {
            ReplyToClient(sender, "550 Directory already exists");
        }

    }

    private void FEAT(TCPClientConnection sender)
    {
        String str = "211-Extensions supported:\r\n";
        str += "PASV\r\n";
        str += "MLST type*;size*;sizd*;modify*;UNIX.mode*;UNIX.uid*;UNIX.gid*;unique*;\r\n";
        str += "211 End.";

        ReplyToClient(sender, str);

        /*Response:	211-Extensions supported:
         Response:	 EPRT
         Response:	 IDLE
         Response:	 MDTM
         Response:	 SIZE
         Response:	 MFMT
         Response:	 REST STREAM
         Response:	 MLST type*;size*;sizd*;modify*;UNIX.mode*;UNIX.uid*;UNIX.gid*;unique*;
         Response:	 MLSD
         Response:	 AUTH TLS
         Response:	 PBSZ
         Response:	 PROT
         Response:	 ESTA
         Response:	 PASV
         Response:	 EPSV
         Response:	 SPSV
         Response:	 ESTP
         Response:	211 End.
         */
    }

    private void CDUP(TCPClientConnection sender)
    {
        ClientProperties clientProperties = (ClientProperties) sender.getTag();
        String currentPath = clientProperties.fCurrentDirectoryPath;

        String path = new File(currentPath).getParent();
        if (CheckIfClientCanAccessDirectory(path))
        {
            clientProperties.fCurrentDirectoryPath = path;
            sender.setTag(clientProperties);
            ReplyToClient(sender, "230 OK!");
        }
        else
        {
            ReplyToClient(sender, "257 " + GetVirtualFTPPathFromSystemPath(fRootPath) + " is your current location");
        }
    }

    private void CWD(TCPClientConnection sender, String response)
    {
        String newFolderName = response.substring(4);

        ClientProperties clientProperties = (ClientProperties) sender.getTag();
        String newFolderPath = "";

        // Get system part from the one given from FTP Client
        if (newFolderName.contains("/"))
        {
            String[] pathParts = newFolderName.split("/");
            if (pathParts.length == 0)
            {
                pathParts = new String[1];
            }

            // First part is root
            pathParts[0] = fRootPath;

            String path = pathParts[0];
            for (int i = 1; i < pathParts.length; i++)
            {
                path += File.separator + pathParts[i];
            }

            newFolderPath = path;
        }
        else
        {
            newFolderPath = clientProperties.fCurrentDirectoryPath + File.separator + newFolderName;
        }

        if (new File(newFolderPath).exists() && new File(newFolderPath).isDirectory())
        {
            clientProperties.fCurrentDirectoryPath = newFolderPath;
            sender.setTag(clientProperties);
            ReplyToClient(sender, "230 OK");
        }
        else
        {
            clientProperties.fCurrentDirectoryPath = fRootPath;
            sender.setTag(clientProperties);
            ReplyToClient(sender, "257 " + GetVirtualFTPPathFromSystemPath(fRootPath) + " is your current location");
            //ReplyToClient(sender, "550 Failed to change directory.");
        }
    }

    private int GetRandomFreeTCPPort() throws IOException
    {
        try
        {
            int port;
            try (ServerSocket server = new ServerSocket(0))
            {
                port = server.getLocalPort();
            }
            return port;
        }
        catch (Exception ex)
        {
        }

        return -1;


    }

    public String GetSystemPathFromVirtualFTPPath(String ftpPath)
    {
        ftpPath = ftpPath.replaceFirst("/", fRootPath);
        ftpPath = ftpPath.replace("/", File.separator);
        return ftpPath;
    }

    public String GetVirtualFTPPathFromSystemPath(String ftpPath)
    {
        ftpPath = ftpPath.replace(fRootPath, "/");
        ftpPath = ftpPath.replace(File.separator, "/");
        return ftpPath;
    }

    private Boolean CheckIfClientCanAccessDirectory(String path)
    {
        return path.startsWith(fRootPath);
    }

    public void ReplyToClient(TCPClientConnection sender, String data)
    {
        try
        {
            if (sender != null)
            {
                data = data + "\r\n";
                byte[] bytes = data.getBytes(UTF8_CHARSET);
                sender.SendData(bytes, 0, bytes.length);
            }
            else
            {
                System.err.println("FTPServer.ReplyToClient Error: sender is null");
            }
        }
        catch (ClientIsDisconnectedException | OutgoingPacketFailedException ex)
        {
            System.err.println("FTPServer.ReplyToClient Error:" + ex.getMessage());
        }
    }

    public void ReplyToClientSynchronously(TCPClientConnection sender, String data)
    {
        try
        {
            if (sender != null)
            {
                data = data + "\r\n";
                byte[] bytes = data.getBytes(UTF8_CHARSET);
                sender.SendDataSynchronous(bytes, 0, bytes.length);
            }
            else
            {
                System.err.println("FTPServer.ReplyToClientSynchronously Error: sender is null");
            }
        }
        catch (Exception ex)
        {
            System.err.println("FTPServer.ReplyToClientSynchronously Error:" + ex.getMessage());
        }
    }
}
